package me.zd.tool.hardlink.service;

import me.zd.tool.hardlink.model.LinkSetting;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class HardLinkService {
    /**
     * 关闭测试模式
     */
    public static final int TEST_MODE_OFF = 0;
    /**
     * 仅测试第一个结果
     */
    public static final int TEST_MODE_FIRST_ONLY = 1;
    /**
     * 测试全部
     */
    public static final int TEST_MODE_ALL = 2;
    public static final HardLinkService INS = new HardLinkService();
    private final DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-M-d H:m:s");

    /**
     * 创建硬链接
     *
     * @param setting  设置
     * @param testMode 测试模式，开启测试模式不创建任何实际的链接
     * @param monitor  监视器，用于返回过程中的信息
     * @return 已连接成功的文件与目标文件映射集合
     * @throws IOException 硬链接异常时抛出
     */
    public Map<File, File> createHardLink(LinkSetting setting, int testMode, Consumer<String> monitor) throws IOException {
        File fromPath = new File(setting.getFromPath());
        if (!fromPath.isDirectory()) {
            monitor.accept(I18nService.INS.format("processing.source.not.dictionary", setting.getFromPath()));
            return null;
        }
        fromPath = fromPath.getAbsoluteFile();
        File destFolder = new File(setting.getToPath()).getAbsoluteFile();
        Context ctx = new Context(setting, monitor, testMode, false);
        scanAndLink(ctx, 1, fromPath, destFolder);
        if (testMode != TEST_MODE_OFF) {
            if (ctx.allDone) {
                if (ctx.linkedFileMap.isEmpty()) {
                    if (ctx.pattern != null) {
                        ctx.monitor.accept(I18nService.INS.format("processing.test.find.zero.with.regexp", ctx.firstInput));
                    } else {
                        ctx.monitor.accept(I18nService.INS.get("processing.test.find.zero"));
                    }
                } else if (testMode == TEST_MODE_FIRST_ONLY) {
                    Map.Entry<File, File> entry = ctx.linkedFileMap.entrySet().iterator().next();
                    File file = entry.getKey();
                    File destFile = entry.getValue();
                    if (ctx.pattern != null) {
                        ctx.monitor.accept(I18nService.INS.format("processing.test.find.one.with.regexp",
                                file.getAbsolutePath(), destFile.getAbsolutePath(),
                                ctx.lastMatchedInput, ctx.lastOutput));
                    } else {
                        ctx.monitor.accept(I18nService.INS.format("processing.test.find.one",
                                file.getAbsolutePath(), destFile.getAbsolutePath()));
                    }
                }
            }
        } else {
            if (ctx.allDone) {
                monitor.accept(I18nService.INS.format("processing.done", ctx.linkedFileMap.size()));
            }
        }
        return ctx.linkedFileMap;
    }

    /**
     * 扫描并链接
     *
     * @param ctx        上下文
     * @param depth      当前搜索深度
     * @param srcFolder  当前深度的来源目录
     * @param destFolder 当前深度的目标目录
     * @throws IOException 创建硬链接异常时抛出
     */
    private void scanAndLink(Context ctx, int depth, File srcFolder, File destFolder) throws IOException {
        File[] files = srcFolder.listFiles();
        if (files == null) {
            return;
        }
        for (int i = 0; i < files.length; i++) {
            if (ctx.interrupt) {
                break;
            }
            File file = files[i];
            if (file.isDirectory()) {
                if (depth < ctx.setting.getScanDepth()) {
                    scanAndLink(ctx, depth + 1, file,
                            new File(destFolder.getAbsolutePath() + File.separator + file.getName()));
                }
                continue;
            }

            if (!checkSuffix(file.getName(), ctx.includeSuffixAry, ctx.excludeSuffixAry)) {
                continue;
            }

            if (ctx.setting.isUsingModifiedTime()) {
                long lastModified = file.lastModified();
                if (lastModified < ctx.beginModifiedTime || lastModified > ctx.endModifiedTime) {
                    continue;
                }
            }

            String destFilePath;
            if (ctx.pattern != null) {
                StringBuilder checkSB = new StringBuilder(file.getName());
                int parentLevel = 0;
                File checkFile = file;
                if (ctx.setting.isUsingRegexpWithParentLevel()) {
                    while (parentLevel < ctx.setting.getRegexpWithParentLevel()) {
                        checkFile = checkFile.getParentFile();
                        checkSB.insert(0, File.separator);
                        checkSB.insert(0, checkFile.getName());
                        parentLevel++;
                    }
                }
                String inputStr = checkSB.toString();
                if (ctx.firstInput == null) {
                    ctx.firstInput = inputStr;
                }
                Matcher matcher = ctx.pattern.matcher(inputStr);
                if (!matcher.find()) {
                    continue;
                }

                File destFolderReal = destFolder;
                if (ctx.setting.isUsingRegexpWithParentLevel()) {
                    parentLevel = 0;
                    while (parentLevel < ctx.setting.getRegexpWithParentLevel()) {
                        destFolderReal = destFolderReal.getParentFile();
                        parentLevel++;
                    }
                }
                String outputStr;
                if (ctx.setting.isUsingRegexpReplacement()) {
                    outputStr = matcher.replaceAll(ctx.setting.getToPattern());
                } else {
                    outputStr = inputStr;
                }
                ctx.lastMatchedInput = inputStr;
                ctx.lastOutput = outputStr;
                destFilePath = destFolderReal.getAbsolutePath() + File.separator + outputStr;
            } else {
                if (ctx.firstInput == null) {
                    ctx.firstInput = file.getName();
                }
                destFilePath = destFolder.getAbsolutePath() + File.separator + file.getName();
            }

            File destFile = new File(destFilePath);
            if (ctx.setting.getTargetFilePrefix() != null && ctx.setting.getTargetFilePrefix().length() > 0) {
                destFile = new File(destFile.getParent() + File.separator + ctx.setting.getTargetFilePrefix()
                        + destFile.getName());
            }

            if (ctx.testMode != TEST_MODE_OFF) {
                // 测试，不进行任何操作
                ctx.linkedFileMap.put(file, destFile);
                if (ctx.testMode == TEST_MODE_FIRST_ONLY) {
                    ctx.interrupt = true;
                }
                continue;
            }

            if (destFile.exists()) {
                ctx.monitor.accept(I18nService.INS.format("processing.target.already.exists",
                        file.getAbsolutePath(), destFile.getAbsolutePath()));
                continue;
            }

            File parentPath = destFile.getParentFile();
            if (parentPath != null) {
                // 检查父级目录
                if (!parentPath.exists()) {
                    // 创建不存在的目录
                    if (!parentPath.mkdirs()) {
                        ctx.monitor.accept(I18nService.INS.format("processing.target.mkdir.failed", parentPath.getAbsolutePath()));
                        continue;
                    }
                } else if (!parentPath.isDirectory()) {
                    // 父级指向不是目录
                    ctx.monitor.accept(I18nService.INS.format("processing.target.not.dictionary", parentPath.getAbsolutePath()));
                    continue;
                }
            }

            // 创建链接
            Files.createLink(destFile.toPath(), file.toPath());
            ctx.monitor.accept(I18nService.INS.format("processing.linked", file.getAbsolutePath(), destFile.getAbsolutePath()));
            ctx.linkedFileMap.put(file, destFile);
        }
        if (depth == 1) {
            ctx.allDone = true;
        }
    }

    private boolean checkSuffix(String fileName, String[] includes, String[] excludes) {
        if (includes != null && includes.length > 0) {
            boolean find = false;
            for (int i = 0; i < includes.length; i++) {
                if (fileName.endsWith(includes[i])) {
                    find = true;
                    break;
                }
            }
            if (!find) {
                return false;
            }
        }
        if (excludes != null && excludes.length > 0) {
            for (int i = 0; i < excludes.length; i++) {
                if (fileName.endsWith(excludes[i])) {
                    return false;
                }
            }
        }
        return true;
    }

    /**
     * 找到第一个，无视正则表达式
     * @param setting 设置
     * @param monitor  监视器，用于返回过程中的信息
     * @return 找到的第一个来源文件
     * @throws IOException 硬链接异常时抛出
     */
    public File findFirstIgnoreRegexp(LinkSetting setting, Consumer<String> monitor) throws IOException {
        File fromPath = new File(setting.getFromPath());
        if (!fromPath.isDirectory()) {
            monitor.accept(I18nService.INS.format("processing.source.not.dictionary", setting.getFromPath()));
            return null;
        }
        fromPath = fromPath.getAbsoluteFile();

        File destFolder = new File(setting.getToPath()).getAbsoluteFile();
        Context ctx = new Context(setting, monitor, TEST_MODE_FIRST_ONLY, true);
        scanAndLink(ctx, 1, fromPath, destFolder);
        return ctx.linkedFileMap.isEmpty() ? null : ctx.linkedFileMap.keySet().iterator().next();
    }

    /**
     * 上下文
     */
    static class Context {
        final LinkSetting setting; // 设置
        // ----解析后的设置----
        final Pattern pattern; // 已编译的正则表达式匹配模式
        final Consumer<String> monitor; // 监视器
        final String[] includeSuffixAry; // 包含后缀
        final String[] excludeSuffixAry; // 排除后缀
        final int testMode; // 测试模式
        final long beginModifiedTime; // 起始修改时间
        final long endModifiedTime; // 结束修改时间
        // ----过程与结果----
        final Map<File, File> linkedFileMap = new LinkedHashMap<>(); // 链接结果集合
        boolean allDone; // 全部完成
        boolean interrupt; // 中断
        String firstInput; // 首个输入
        String lastMatchedInput; // 最后匹配输入
        String lastOutput; // 最后输出

        public Context(LinkSetting setting, Consumer<String> monitor, int testMode, boolean ignoreRegexp) {
            this.setting = setting;
            this.monitor = monitor;
            this.testMode = testMode;
            if (!ignoreRegexp && setting.isUsingRegexp() && setting.getFromPattern() != null && !setting.getFromPattern().isEmpty()) {
                this.pattern = Pattern.compile(setting.getFromPattern());
            } else {
                this.pattern = null;
            }
            if (setting.getIncludeSuffix() != null && setting.getIncludeSuffix().length() > 0) {
                includeSuffixAry = setting.getIncludeSuffix().split("\\|");
            } else {
                includeSuffixAry = null;
            }
            if (setting.getExcludeSuffix() != null && setting.getExcludeSuffix().length() > 0) {
                excludeSuffixAry = setting.getExcludeSuffix().split("\\|");
            } else {
                excludeSuffixAry = null;
            }
            if (setting.isUsingModifiedTime()) {
                BiFunction<String, Long, Long> transFunc = (text, defaultValue) -> {
                    if (text == null || text.isEmpty()) {
                        return defaultValue;
                    }
                    try {
                        LocalDateTime dateTime = LocalDateTime.parse(text, INS.dateTimeFormatter);
                        return dateTime.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();
                    } catch (DateTimeParseException e) {
                        return defaultValue;
                    }
                };
                beginModifiedTime = transFunc.apply(setting.getBeginModifiedTime(), 0L);
                endModifiedTime = transFunc.apply(setting.getEndModifiedTime(), Long.MAX_VALUE);
            } else {
                beginModifiedTime = 0;
                endModifiedTime = 0;
            }
        }
    }
}
